<?php
/**
 * The OpenID and Yadis discovery implementation for OpenID 1.2.
 */

require_once "Auth/OpenID.php";
require_once "Auth/OpenID/Parse.php";
require_once "Auth/OpenID/Message.php";
require_once "Auth/Yadis/XRIRes.php";
require_once "Auth/Yadis/Yadis.php";

// XML namespace value
define('Auth_OpenID_XMLNS_1_0', 'http://openid.net/xmlns/1.0');

// Yadis service types
define('Auth_OpenID_TYPE_1_2', 'http://openid.net/signon/1.2');
define('Auth_OpenID_TYPE_1_1', 'http://openid.net/signon/1.1');
define('Auth_OpenID_TYPE_1_0', 'http://openid.net/signon/1.0');
define('Auth_OpenID_TYPE_2_0_IDP', 'http://specs.openid.net/auth/2.0/server');
define('Auth_OpenID_TYPE_2_0', 'http://specs.openid.net/auth/2.0/signon');
define('Auth_OpenID_RP_RETURN_TO_URL_TYPE', 'http://specs.openid.net/auth/2.0/return_to');

function Auth_OpenID_getOpenIDTypeURIs() {
  return array(Auth_OpenID_TYPE_2_0_IDP, Auth_OpenID_TYPE_2_0, Auth_OpenID_TYPE_1_2, Auth_OpenID_TYPE_1_1, 
      Auth_OpenID_TYPE_1_0, Auth_OpenID_RP_RETURN_TO_URL_TYPE);
}

/**
 * Object representing an OpenID service endpoint.
 */
class Auth_OpenID_ServiceEndpoint {

  function Auth_OpenID_ServiceEndpoint() {
    $this->claimed_id = null;
    $this->server_url = null;
    $this->type_uris = array();
    $this->local_id = null;
    $this->canonicalID = null;
    $this->used_yadis = false; // whether this came from an XRDS
    $this->display_identifier = null;
  }

  function getDisplayIdentifier() {
    if ($this->display_identifier) {
      return $this->display_identifier;
    }
    if (! $this->claimed_id) {
      return $this->claimed_id;
    }
    $parsed = parse_url($this->claimed_id);
    $scheme = $parsed['scheme'];
    $host = $parsed['host'];
    $path = $parsed['path'];
    if (array_key_exists('query', $parsed)) {
      $query = $parsed['query'];
      $no_frag = "$scheme://$host$path?$query";
    } else {
      $no_frag = "$scheme://$host$path";
    }
    return $no_frag;
  }

  function usesExtension($extension_uri) {
    return in_array($extension_uri, $this->type_uris);
  }

  function preferredNamespace() {
    if (in_array(Auth_OpenID_TYPE_2_0_IDP, $this->type_uris) || in_array(Auth_OpenID_TYPE_2_0, $this->type_uris)) {
      return Auth_OpenID_OPENID2_NS;
    } else {
      return Auth_OpenID_OPENID1_NS;
    }
  }

  /*
     * Query this endpoint to see if it has any of the given type
     * URIs. This is useful for implementing other endpoint classes
     * that e.g. need to check for the presence of multiple versions
     * of a single protocol.
     *
     * @param $type_uris The URIs that you wish to check
     *
     * @return all types that are in both in type_uris and
     * $this->type_uris
     */
  function matchTypes($type_uris) {
    $result = array();
    foreach ($type_uris as $test_uri) {
      if ($this->supportsType($test_uri)) {
        $result[] = $test_uri;
      }
    }
    
    return $result;
  }

  function supportsType($type_uri) {
    // Does this endpoint support this type?
    return ((in_array($type_uri, $this->type_uris)) || (($type_uri == Auth_OpenID_TYPE_2_0) && $this->isOPIdentifier()));
  }

  function compatibilityMode() {
    return $this->preferredNamespace() != Auth_OpenID_OPENID2_NS;
  }

  function isOPIdentifier() {
    return in_array(Auth_OpenID_TYPE_2_0_IDP, $this->type_uris);
  }

  function fromOPEndpointURL($op_endpoint_url) {
    // Construct an OP-Identifier OpenIDServiceEndpoint object for
    // a given OP Endpoint URL
    $obj = new Auth_OpenID_ServiceEndpoint();
    $obj->server_url = $op_endpoint_url;
    $obj->type_uris = array(Auth_OpenID_TYPE_2_0_IDP);
    return $obj;
  }

  function parseService($yadis_url, $uri, $type_uris, $service_element) {
    // Set the state of this object based on the contents of the
    // service element.  Return true if successful, false if not
    // (if findOPLocalIdentifier returns false).
    $this->type_uris = $type_uris;
    $this->server_url = $uri;
    $this->used_yadis = true;
    
    if (! $this->isOPIdentifier()) {
      $this->claimed_id = $yadis_url;
      $this->local_id = Auth_OpenID_findOPLocalIdentifier($service_element, $this->type_uris);
      if ($this->local_id === false) {
        return false;
      }
    }
    
    return true;
  }

  function getLocalID() {
    // Return the identifier that should be sent as the
    // openid.identity_url parameter to the server.
    if ($this->local_id === null && $this->canonicalID === null) {
      return $this->claimed_id;
    } else {
      if ($this->local_id) {
        return $this->local_id;
      } else {
        return $this->canonicalID;
      }
    }
  }

  /*
     * Parse the given document as XRDS looking for OpenID services.
     *
     * @return array of Auth_OpenID_ServiceEndpoint or null if the
     * document cannot be parsed.
     */
  function fromXRDS($uri, $xrds_text) {
    $xrds = & Auth_Yadis_XRDS::parseXRDS($xrds_text);
    
    if ($xrds) {
      $yadis_services = $xrds->services(array('filter_MatchesAnyOpenIDType'));
      return Auth_OpenID_makeOpenIDEndpoints($uri, $yadis_services);
    }
    
    return null;
  }

  /*
     * Create endpoints from a DiscoveryResult.
     *
     * @param discoveryResult Auth_Yadis_DiscoveryResult
     * @return array of Auth_OpenID_ServiceEndpoint or null if
     * endpoints cannot be created.
     */
  function fromDiscoveryResult($discoveryResult) {
    if ($discoveryResult->isXRDS()) {
      return Auth_OpenID_ServiceEndpoint::fromXRDS($discoveryResult->normalized_uri, $discoveryResult->response_text);
    } else {
      return Auth_OpenID_ServiceEndpoint::fromHTML($discoveryResult->normalized_uri, $discoveryResult->response_text);
    }
  }

  function fromHTML($uri, $html) {
    $discovery_types = array(
        array(Auth_OpenID_TYPE_2_0, 'openid2.provider', 'openid2.local_id'), 
        array(Auth_OpenID_TYPE_1_1, 'openid.server', 'openid.delegate'));
    
    $services = array();
    
    foreach ($discovery_types as $triple) {
      list($type_uri, $server_rel, $delegate_rel) = $triple;
      
      $urls = Auth_OpenID_legacy_discover($html, $server_rel, $delegate_rel);
      
      if ($urls === false) {
        continue;
      }
      
      list($delegate_url, $server_url) = $urls;
      
      $service = new Auth_OpenID_ServiceEndpoint();
      $service->claimed_id = $uri;
      $service->local_id = $delegate_url;
      $service->server_url = $server_url;
      $service->type_uris = array($type_uri);
      
      $services[] = $service;
    }
    
    return $services;
  }

  function copy() {
    $x = new Auth_OpenID_ServiceEndpoint();
    
    $x->claimed_id = $this->claimed_id;
    $x->server_url = $this->server_url;
    $x->type_uris = $this->type_uris;
    $x->local_id = $this->local_id;
    $x->canonicalID = $this->canonicalID;
    $x->used_yadis = $this->used_yadis;
    
    return $x;
  }
}

function Auth_OpenID_findOPLocalIdentifier($service, $type_uris) {
  // Extract a openid:Delegate value from a Yadis Service element.
  // If no delegate is found, returns null.  Returns false on
  // discovery failure (when multiple delegate/localID tags have
  // different values).
  

  $service->parser->registerNamespace('openid', Auth_OpenID_XMLNS_1_0);
  
  $service->parser->registerNamespace('xrd', Auth_Yadis_XMLNS_XRD_2_0);
  
  $parser = & $service->parser;
  
  $permitted_tags = array();
  
  if (in_array(Auth_OpenID_TYPE_1_1, $type_uris) || in_array(Auth_OpenID_TYPE_1_0, $type_uris)) {
    $permitted_tags[] = 'openid:Delegate';
  }
  
  if (in_array(Auth_OpenID_TYPE_2_0, $type_uris)) {
    $permitted_tags[] = 'xrd:LocalID';
  }
  
  $local_id = null;
  
  foreach ($permitted_tags as $tag_name) {
    $tags = $service->getElements($tag_name);
    
    foreach ($tags as $tag) {
      $content = $parser->content($tag);
      
      if ($local_id === null) {
        $local_id = $content;
      } else if ($local_id != $content) {
        return false;
      }
    }
  }
  
  return $local_id;
}

function filter_MatchesAnyOpenIDType(&$service) {
  $uris = $service->getTypes();
  
  foreach ($uris as $uri) {
    if (in_array($uri, Auth_OpenID_getOpenIDTypeURIs())) {
      return true;
    }
  }
  
  return false;
}

function Auth_OpenID_bestMatchingService($service, $preferred_types) {
  // Return the index of the first matching type, or something
  // higher if no type matches.
  //
  // This provides an ordering in which service elements that
  // contain a type that comes earlier in the preferred types list
  // come before service elements that come later. If a service
  // element has more than one type, the most preferred one wins.
  

  foreach ($preferred_types as $index => $typ) {
    if (in_array($typ, $service->type_uris)) {
      return $index;
    }
  }
  
  return count($preferred_types);
}

function Auth_OpenID_arrangeByType($service_list, $preferred_types) {
  // Rearrange service_list in a new list so services are ordered by
  // types listed in preferred_types.  Return the new list.
  

  // Build a list with the service elements in tuples whose
  // comparison will prefer the one with the best matching service
  $prio_services = array();
  foreach ($service_list as $index => $service) {
    $prio_services[] = array(
        Auth_OpenID_bestMatchingService($service, $preferred_types), $index, $service);
  }
  
  sort($prio_services);
  
  // Now that the services are sorted by priority, remove the sort
  // keys from the list.
  foreach ($prio_services as $index => $s) {
    $prio_services[$index] = $prio_services[$index][2];
  }
  
  return $prio_services;
}

// Extract OP Identifier services.  If none found, return the rest,
// sorted with most preferred first according to
// OpenIDServiceEndpoint.openid_type_uris.
//
// openid_services is a list of OpenIDServiceEndpoint objects.
//
// Returns a list of OpenIDServiceEndpoint objects."""
function Auth_OpenID_getOPOrUserServices($openid_services) {
  $op_services = Auth_OpenID_arrangeByType($openid_services, array(Auth_OpenID_TYPE_2_0_IDP));
  
  $openid_services = Auth_OpenID_arrangeByType($openid_services, Auth_OpenID_getOpenIDTypeURIs());
  
  if ($op_services) {
    return $op_services;
  } else {
    return $openid_services;
  }
}

function Auth_OpenID_makeOpenIDEndpoints($uri, $yadis_services) {
  $s = array();
  
  if (! $yadis_services) {
    return $s;
  }
  
  foreach ($yadis_services as $service) {
    $type_uris = $service->getTypes();
    $uris = $service->getURIs();
    
    // If any Type URIs match and there is an endpoint URI
    // specified, then this is an OpenID endpoint
    if ($type_uris && $uris) {
      foreach ($uris as $service_uri) {
        $openid_endpoint = new Auth_OpenID_ServiceEndpoint();
        if ($openid_endpoint->parseService($uri, $service_uri, $type_uris, $service)) {
          $s[] = $openid_endpoint;
        }
      }
    }
  }
  
  return $s;
}

function Auth_OpenID_discoverWithYadis($uri, &$fetcher, $endpoint_filter = 'Auth_OpenID_getOPOrUserServices', $discover_function = null) {
  // Discover OpenID services for a URI. Tries Yadis and falls back
  // on old-style <link rel='...'> discovery if Yadis fails.
  

  // Might raise a yadis.discover.DiscoveryFailure if no document
  // came back for that URI at all.  I don't think falling back to
  // OpenID 1.0 discovery on the same URL will help, so don't bother
  // to catch it.
  if ($discover_function === null) {
    $discover_function = array('Auth_Yadis_Yadis', 'discover');
  }
  
  $openid_services = array();
  
  $response = call_user_func_array($discover_function, array($uri, &$fetcher));
  
  $yadis_url = $response->normalized_uri;
  $yadis_services = array();
  
  if ($response->isFailure()) {
    return array($uri, array());
  }
  
  $openid_services = Auth_OpenID_ServiceEndpoint::fromXRDS($yadis_url, $response->response_text);
  
  if (! $openid_services) {
    if ($response->isXRDS()) {
      return Auth_OpenID_discoverWithoutYadis($uri, $fetcher);
    }
    
    // Try to parse the response as HTML to get OpenID 1.0/1.1
    // <link rel="...">
    $openid_services = Auth_OpenID_ServiceEndpoint::fromHTML($yadis_url, $response->response_text);
  }
  
  $openid_services = call_user_func_array($endpoint_filter, array(&$openid_services));
  
  return array($yadis_url, $openid_services);
}

function Auth_OpenID_discoverURI($uri, &$fetcher) {
  $uri = Auth_OpenID::normalizeUrl($uri);
  return Auth_OpenID_discoverWithYadis($uri, $fetcher);
}

function Auth_OpenID_discoverWithoutYadis($uri, &$fetcher) {
  $http_resp = @$fetcher->get($uri);
  
  if ($http_resp->status != 200 and $http_resp->status != 206) {
    return array($uri, array());
  }
  
  $identity_url = $http_resp->final_url;
  
  // Try to parse the response as HTML to get OpenID 1.0/1.1 <link
  // rel="...">
  $openid_services = Auth_OpenID_ServiceEndpoint::fromHTML($identity_url, $http_resp->body);
  
  return array($identity_url, $openid_services);
}

function Auth_OpenID_discoverXRI($iname, &$fetcher) {
  $resolver = new Auth_Yadis_ProxyResolver($fetcher);
  list($canonicalID, $yadis_services) = $resolver->query($iname, Auth_OpenID_getOpenIDTypeURIs(), array(
      'filter_MatchesAnyOpenIDType'));
  
  $openid_services = Auth_OpenID_makeOpenIDEndpoints($iname, $yadis_services);
  
  $openid_services = Auth_OpenID_getOPOrUserServices($openid_services);
  
  for ($i = 0; $i < count($openid_services); $i ++) {
    $openid_services[$i]->canonicalID = $canonicalID;
    $openid_services[$i]->claimed_id = $canonicalID;
    $openid_services[$i]->display_identifier = $iname;
  }
  
  // FIXME: returned xri should probably be in some normal form
  return array($iname, $openid_services);
}

function Auth_OpenID_discover($uri, &$fetcher) {
  // If the fetcher (i.e., PHP) doesn't support SSL, we can't do
  // discovery on an HTTPS URL.
  if ($fetcher->isHTTPS($uri) && ! $fetcher->supportsSSL()) {
    return array($uri, array());
  }
  
  if (Auth_Yadis_identifierScheme($uri) == 'XRI') {
    $result = Auth_OpenID_discoverXRI($uri, $fetcher);
  } else {
    $result = Auth_OpenID_discoverURI($uri, $fetcher);
  }
  
  // If the fetcher doesn't support SSL, we can't interact with
  // HTTPS server URLs; remove those endpoints from the list.
  if (! $fetcher->supportsSSL()) {
    $http_endpoints = array();
    list($new_uri, $endpoints) = $result;
    
    foreach ($endpoints as $e) {
      if (! $fetcher->isHTTPS($e->server_url)) {
        $http_endpoints[] = $e;
      }
    }
    
    $result = array($new_uri, $http_endpoints);
  }
  
  return $result;
}
